#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import os
import signal
import sys
import shutil
import json
import locale
import subprocess
import shlex
import time
from threading import Thread
import psutil
from exchange import log

from exchange.commons.common import read_temp_file, check_del_dir_or_file, output_action_result, \
    output_result_file, get_key_value
from exchange.commons.const import ExchangeCode, ParamConstant, BodyErr, ExecCmdResult, JobBackupType, \
    ExchangeWindowsPath, VSSToolOperation, TaskStage
from common.common import execute_cmd, exter_attack, get_host_sn
from exchange.commons.exchange_param_parse import ExchangeParamParse
from common.const import SysData, SubJobStatusEnum, RepositoryDataTypeEnum, SubJobPolicyEnum, \
    SubJobTypeEnum
from common.cleaner import clear
from common.common_models import SubJobModel
from exchange.backup.exchange_backup_base import ExchangeBackupBase


def get_command_result(command):
    tmp_code = locale.getdefaultlocale()[1]
    if "|" in command:
        raise Exception("Special characters exist.")
    cmds = shlex.split(command)
    process = subprocess.Popen(cmds, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE, encoding=tmp_code, errors="ignore")
    process.wait()
    code, out, err = str(process.returncode), process.stdout.read(), process.stderr.read()
    return code, out, err


def get_snap_id(snap_infos, device):
    for snap_info_data in snap_infos['snapInfo']:
        device_letter = snap_info_data['deviceLetter']
        volume_id = snap_info_data['volumeId']
        if device_letter[0] == device:
            return volume_id
    return ""


class ExchangeBackupComm(ExchangeBackupBase):
    m_func_type = ""
    m_check_output = {'code': ExchangeCode.FAILED.value, 'bodyErr': 0, 'message': ''}

    def __init__(self, pid, job_id, sub_job_id):
        log.info("init start!")
        self._exchange_object = None
        self._database_type = ""
        self.pid = pid
        self._job_id = job_id
        self._sub_job_id = sub_job_id
        self.m_username = ""
        self.m_password = ""
        self.writer_name = "Microsoft Exchange Writer"
        self.guid = ""
        self.data_hub = ""
        self.meta_hub = ""
        self.m_progress = 50
        self.m_progress_status = SubJobStatusEnum.RUNNING.value
        self.job_type = ""

        file_path = os.path.join(ParamConstant.PARAM_FILE_PATH, f"param{self.pid}")
        json_param = read_temp_file(file_path)
        check_del_dir_or_file(file_path)
        self._json_param = json_param
        super().__init__(self.pid, self._job_id, self._sub_job_id, self._json_param)

    @staticmethod
    def get_target_path(data_hub, guid, meta_hub):
        input_data_hub = os.path.join(data_hub, f"MailboxDatabase_{guid}").replace("/", "\\")
        input_meta_hub = os.path.join(meta_hub, f"MailboxDatabase_{guid}").replace("/", "\\")
        return input_data_hub, input_meta_hub

    # 备份鉴权
    @exter_attack
    def allow_backup_in_local_node(self):
        """
        功能描述：是否允许本地运行
        查询是否有可以备份的数据库：
        数据状态为已挂载，且状态为healthy
        """
        log.info("allow_backup_in_local_node start!")
        healthy_database, drive_letter = self.get_databases()
        if not healthy_database:
            log.error("allow_backup_in_local_node FAILED!")
            output_action_result(self.pid, ExchangeCode.FAILED.value, BodyErr.ERR_DB_SERVICES, "")
            return False
        log.info("allow_backup_in_local_node SUCCESS!")
        output_action_result(self.pid, ExchangeCode.SUCCESS.value, 0, "")
        return True

    def get_databases(self):
        """
        功能描述：获取数据库信息
        查询是否有可以备份的数据库：
        数据状态为已挂载，且状态为healthy
        """
        log.info("Enter get_database")
        # "\" 转为 "\\"
        username = self.m_username.replace("\\", "\\\\")
        # 获取单机或DGA上所有已挂载的数据库
        ps1_path = ParamConstant.PS1_SCRIPTS_PATH + "GetAllMountedMailboxDatabases.ps1"
        cmds = f"{ParamConstant.PS_LOC} {ps1_path} \\\"{username}\\\" \\\"{self.m_password}\\\""
        ret, std_out, std_err = execute_cmd(cmds)
        result = '\n'.join(std_out.splitlines()[-1:])
        result = json.loads(result)
        return_code = result.get("ReturnCode", "")
        resource_list = []
        # 盘符列表
        drive_letter = []
        if return_code != "0":
            return resource_list, drive_letter
        if result.get("Output", []):
            for temp in result.get("Output"):
                resource = {
                    "type": "Database", "subType": "Exchange-database", "uuid": temp["db_uuid"], "extendInfo": temp
                }
                drive_letter.append(temp["edb_path"][0])
                drive_letter.append(temp["log_path"][0])
                resource_list.append(resource)
        drive_letter = list(set(drive_letter))
        return resource_list, drive_letter

    @exter_attack
    def check_backup_job_type(self):
        """
        功能描述：检查任务备份类型：
        全量备份接口，无需检查
        """
        log.info("check_backup_job_type!")
        output_action_result(self.pid, ExchangeCode.SUCCESS.value, 0, "")
        log.info("check_backup_job_type SUCCESSES")
        return True

    # 前置任务
    @exter_attack
    def backup_prerequisite(self):
        """
        功能描述：备份任务前置任务
        初始化数据仓信息
        """
        log.info("backup_prerequisite start!")
        self.m_progress = 50
        self.m_progress_status = SubJobStatusEnum.RUNNING.value
        output_action_result(self.pid, ExchangeCode.SUCCESS.value, 0, "")
        self.write_progress_to_file(SubJobStatusEnum.COMPLETED.value, 100, "", "common")
        log.info("backup_prerequisite SUCCESS!")
        return True

    # 生成子任务
    @exter_attack
    def backup_gen_sub_job(self):
        """
        功能描述：恢复分解子任务, 主任务执行
        根据agentId拆分
        """
        log.info("backup_gen_sub_job start!")
        response = []
        nodes = self._json_param.get("job").get("protectEnv").get("nodes")
        for node in nodes:
            agent_id = node['id']
            response.append(
                SubJobModel(jobId=self._job_id, subJobId=self._sub_job_id,
                            jobType=SubJobTypeEnum.BUSINESS_SUB_JOB.value, jobName="ExchangeFullBackup",
                            policy=SubJobPolicyEnum.FIXED_NODE,
                            execNodeId=agent_id).dict(by_alias=True))
        output_result_file(self.pid, response)
        log.info("backup_gen_sub_job SUCCESS!")
        return True

    # 执行备份
    @exter_attack
    def backup(self):
        """
        执行备份任务
        生成副本格式
        ├── /{copy_id}/source_policy_{policy_id}_Context/{agent_id}/
        │   ├── {MailboxDatabase_guid}
        │   │   └── xx.edb
        │   │   └── x.log
        │   │   └── x.chk
        │   ├── {MailboxDatabase_guid}
        │   │   └── xx.edb
        │   │   └── x.log
        │   │   └── x.chk
        """
        try:
            log.info("Backup Start!")
            # 验证和设置基础备份类型
            self.verifying_and_init()
            # 守护进程，避免被DME误判
            progress_thread = Thread(target=self.report_back_progress_thread,
                                     args=())
            progress_thread.setDaemon(True)
            progress_thread.start()
            # 获取该节点上所有健康的数据库
            databases_info, drive_letter = self.get_databases()
            input_writer_name = "\"" + self.writer_name + "\""
            snap_meta_hub = ExchangeWindowsPath.VSS_PATH_Prefix + ExchangeWindowsPath.UNC + self.meta_hub + "\""
            # 打快照
            vss_exec_param = "{} {} {} {} {}".format(ParamConstant.VSS_TOOL_PATH, self.job_type,
                                                     input_writer_name,
                                                     snap_meta_hub, "\"" + ','.join(drive_letter) + "\"")
            code, out, err = get_command_result(vss_exec_param)
            if code != ExecCmdResult.SUCCESS:
                output_action_result(self.pid, ExchangeCode.FAILED.value, BodyErr.ERR_PLUGIN_CANNOT_BACKUP.value,
                                     "")
                self.write_progress_to_file(SubJobStatusEnum.FAILED.value, 100, "", "common")
                return False

            # 拷贝数据
            # 解析snapInfo.json
            snap_info_path = self.meta_hub + f"\\{self._job_id}.json"
            with open(snap_info_path, 'r') as f_read:
                snap_info = json.load(f_read)
            for database_info in databases_info:
                self.m_progress = self.m_progress + 1
                edb_path = database_info["extendInfo"]["edb_path"]
                log_path = database_info["extendInfo"]["log_path"]
                # 初始化数据仓
                database_guid = database_info["uuid"]
                input_data_hub, input_meta_hub = self.get_target_path(self.data_hub, database_guid, self.meta_hub)
                self.init_cache_and_data_directory(input_data_hub, input_meta_hub)
                # 复制edb文件，获取虚拟盘edb路径
                edb_snap_id = get_snap_id(snap_info, edb_path[0])
                virtual_edb_path = ExchangeWindowsPath.UNC + ParamConstant.VIRTUAL_PATH + f"{edb_snap_id}" + \
                                   edb_path.split(':')[1]
                if not os.path.exists(virtual_edb_path):
                    output_action_result(self.pid, ExchangeCode.FAILED.value, BodyErr.ERR_PLUGIN_CANNOT_BACKUP.value,
                                         "")
                    self.write_progress_to_file(SubJobStatusEnum.FAILED.value, 100, "", "common")
                    return False
                input_data_hub = ExchangeWindowsPath.UNC + input_data_hub
                shutil.copy(virtual_edb_path, input_data_hub)
                # 复制xx.log,xx.chk数据
                self.copy_backup_data(input_data_hub, log_path, snap_info)
            output_action_result(self.pid, ExchangeCode.SUCCESS.value, 0, "")
            self.write_progress_to_file(SubJobStatusEnum.COMPLETED.value, 100, "", "common")
        except Exception as error:
            log.error(error, exc_info=True)
            output_action_result(self.pid, ExchangeCode.FAILED.value, BodyErr.ERR_PLUGIN_CANNOT_BACKUP.value, "")
            self.write_progress_to_file(SubJobStatusEnum.FAILED.value, 100, "", "common")
            return False
        return True

    def verifying_and_init(self):
        self.m_progress = 50
        self.m_progress_status = SubJobStatusEnum.RUNNING.value
        # 验证vss是否开启
        self.check_vss_snap_shot()
        # 初始化路径信息
        self.init_hub_path()
        # 设置备份类型
        self.set_job_type()

    def copy_backup_data(self, input_data_hub, log_path, snap_info):
        # 复制log文件，获取虚拟盘edb路径
        log_snap_id = get_snap_id(snap_info, log_path[0])
        virtual_log_path = ParamConstant.VIRTUAL_PATH + f"{log_snap_id}" + log_path.split(':')[1]
        # 遍历源目录下的所有文件
        for root, _dirs, files in os.walk(virtual_log_path):
            for file in files:
                # 判断文件是否以.edb结尾
                if file.endswith(".log") or file.endswith('.chk'):
                    # 构造源文件路径和目标文件路径
                    src_file = os.path.join(root, file)
                    dst_file = os.path.join(input_data_hub, file)
                    # 复制文件
                    shutil.copy(ExchangeWindowsPath.UNC + src_file, dst_file)
        self.write_progress_to_file(SubJobStatusEnum.RUNNING.value, self.m_progress, "", "common")

    def init_hub_path(self):
        # 获取agent_id
        agent_id = get_host_sn()
        data_hub = ExchangeParamParse.get_data_path(self._json_param)
        self.data_hub = data_hub.replace("/", "\\") + f"\\{agent_id}"

        meta_hub = ExchangeParamParse.get_meta_path(self._json_param)
        self.meta_hub = meta_hub.replace("/", "\\") + f"\\{agent_id}"
        self.init_cache_and_data_directory(self.data_hub, self.meta_hub)
        log.info("create  meta_hub success!")
        return self.data_hub, self.meta_hub

    # 后置任务 删除快照id，清理临时路径下的文件信息
    @exter_attack
    def backup_post_job(self):
        log.info("backup_post_job Start!")
        # 解析snapInfo.json
        self.init_hub_path()
        snap_info_path = ExchangeWindowsPath.UNC + self.meta_hub + f"\\{self._job_id}.json"
        if not os.path.exists(snap_info_path):
            output_action_result(self.pid, ExchangeCode.SUCCESS.value, 0, "")
            self.write_progress_to_file(SubJobStatusEnum.COMPLETED.value, 100, "", "common")
            return True
        with open(snap_info_path, 'r') as f_read:
            snap_infos = json.load(f_read)
        for snap_info_data in snap_infos['snapInfo']:
            volume_id = snap_info_data['volumeId']
            if volume_id is None or len(volume_id) == 0:
                return False
            # 删除快照
            vss_exec_param = "{} {} {}".format(ParamConstant.VSS_TOOL_PATH, VSSToolOperation.DELETE,
                                               volume_id)
            code, out, err = get_command_result(vss_exec_param)
            # 移除功能
            if os.path.exists(ParamConstant.VIRTUAL_PATH + f"{volume_id}"):
                shutil.rmtree(ParamConstant.VIRTUAL_PATH + f"{volume_id}")
            if code != ExecCmdResult.SUCCESS:
                output_action_result(self.pid, ExchangeCode.FAILED.value, BodyErr.ERR_PLUGIN_CANNOT_BACKUP.value,
                                     "")
                self.write_progress_to_file(SubJobStatusEnum.FAILED.value, 100, "", "common")
                return False
        log.info(f"delete snapshot success!")
        output_action_result(self.pid, ExchangeCode.SUCCESS.value, 0, "")
        self.write_progress_to_file(SubJobStatusEnum.COMPLETED.value, 100, "", "common")
        log.info("backup_post_job SUCCESS!")
        return True

    def report_back_progress_thread(self):
        """
        功能描述：单起进程，用于上报复制文件进度，避免超大文件复制超过10分钟，致使DME误判进程死亡
        返回值：无
        """
        log.info(f"start report progress!")
        while True:
            self.write_progress_to_file(SubJobStatusEnum.RUNNING.value, 15, "", "common")
            time.sleep(60)

    @exter_attack
    def query_backup_copy(self):
        """
        功能描述：查询备份副本, 备份成功, 执行后置任务之前执行, 备份失败不会调用
        参数：
        @pid： 请求ID
        @job_id： 主任务任务ID
        返回值：CommonBodyResponse
        """
        copy_info = {}
        try:
            nodes = self._json_param['job']['protectEnv']['nodes']
            copy_id = self._json_param['job']['copy'][0]['id']
            repositories = self._json_param['job']['repositories']
            report_repositories = []
            for repository in repositories:
                repository_type = int(repository['repositoryType'])
                if repository_type == RepositoryDataTypeEnum.META_REPOSITORY.value or \
                        repository_type == RepositoryDataTypeEnum.DATA_REPOSITORY.value:
                    repository['remotePath'] = f"{repository['remotePath']}/{copy_id}"
                    report_repositories.append(repository)
            copy_info['repositories'] = report_repositories
            copy_info['extendInfo'] = {'nodes': nodes, 'copyId': copy_id}
        except Exception as e:
            log.error(e, exc_info=True)
        output_result_file(self.pid, copy_info)

    def set_job_type(self):
        ret = self._json_param.get("job").get("jobParam").get("backupType")
        if ret == JobBackupType.EXCHANGE_BACKUP_FULL:
            self.job_type = "Backup"
            return True
        elif ret == JobBackupType.EXCHANGE_BACKUP_FOREVER_INC:
            if self.check_circular_logging():
                self.job_type = "Log"
                return True
            else:
                log.error("Set job type Failed! check_circular_logging Failed!")
                return False
        else:
            log.error("Set job type Failed! JobBackupType Error!")
            return False

    # 检查是否需要增量转全量
    def check_can_backup_inc(self):
        log.info("check_can_backup_inc start!")

        path = self.data_hub
        folder_name = f"/MailboxDatabase_{self.guid}"
        final_path = path + folder_name
        final_path = final_path.replace("/", "\\")
        try:
            if not os.path.exists(final_path):
                log.error("No Full Backup Copy! Start Inc To Full!")
                return False
            files = os.listdir(final_path);
            for k, _ in enumerate(files):
                # 提取文件夹内所有文件的后缀
                files[k] = os.path.splitext(files[k])[1]
            edbstr = ".edb"
            if edbstr in files:
                log.info("check_can_backup_inc SUCCESS!")
                return True
            else:
                log.error("No Full Backup Copy! Start Inc To Full!")
                return False
        except Exception as error:
            log.error(error, exc_info=True)
            log.error("check_can_backup_inc FAILED!")
            return False

    def get_code(self, input_ret):
        if input_ret:
            return ExchangeCode.SUCCESS.value
        else:
            return ExchangeCode.FAILED.value

    def format_body_err(self, input_ret):
        temp_body_err = 0
        if not input_ret:
            temp_body_err = BodyErr.ERR_PLUGIN_CANNOT_BACKUP.value
        return temp_body_err

    def check_vss_snap_shot(self):
        log.info("check_VSS_snap_shot start!")
        cmds = f"{ParamConstant.PS_LOC} {ParamConstant.PS1_SCRIPTS_PATH}IsVSSWriterStable.ps1"
        ret, std_out, std_err = execute_cmd(cmds)
        result = '\n'.join(std_out.splitlines()[-1:])
        if result == "True":
            return True
        log.error("backup_prerequisite FAILED! check_vss_snap_shot")
        output_action_result(self.pid, ExchangeCode.FAILED.value, BodyErr.ERR_PLUGIN_CANNOT_BACKUP.value, "")
        return False

    def init_cache_and_data_directory(self, data_hub_path, meta_hub_path):
        log.info("init_cache_and_data_directory start!")
        path_list = [data_hub_path, meta_hub_path]
        for path in path_list:
            final_path = ExchangeWindowsPath.UNC + path
            final_path = final_path.replace("/", "\\")
            try:
                if not os.path.exists(final_path):
                    os.makedirs(final_path)
            except Exception as error:
                log.error(error, exc_info=True)
                log.info("init_cache_and_data_directory FAILED!")
                output_action_result(self.pid, ExchangeCode.FAILED.value, BodyErr.ERR_PLUGIN_CANNOT_BACKUP.value,
                                     "")
                raise Exception(f"init_cache_and_data_directory") from error
        log.info("init cache SUCCESS!")
        return True

    @exter_attack
    def query_job_permission(self):
        output_action_result(self.pid, ExchangeCode.SUCCESS.value, 0, "")
        return True

    def get_server_id(self, server_name):
        log.info("get_server_id start!")
        username = self.m_username.replace("\\", "\\\\")
        cmds = f"{ParamConstant.PS_LOC} {ParamConstant.PS1_SCRIPTS_PATH}GetServerGuid.ps1 \
            {username} {self.m_password} {server_name}"
        ret, std_out, std_err = execute_cmd(cmds)
        result = '\n'.join(std_out.splitlines()[-1:])
        return result

    @exter_attack
    def progress_comm(self):
        log.info("progress_com Start!!!")
        self.report_progress_comm()
        log.info("progress_com Finished!!!")
        return True

    @exter_attack
    def abort_job(self):
        """
        功能描述：执行中止任务, 页面触发
        参数：
        @pid： 请求ID
        @job_id： 主任务任务ID
        返回值：CommonBodyResponse
        """
        if self._job_id is None or len(self._job_id) == 0:
            return
        processes = psutil.process_iter()
        for process in processes:
            cmd_lines = process.cmdline()
            if len(cmd_lines) < 5:
                continue
            if str(self._job_id) == str(cmd_lines[4]) and (
                    str(cmd_lines[2]) in [TaskStage.BACKUP_PREREQUISITE, TaskStage.BACKUP_GEN_SUB_JOB,
                                          TaskStage.BACKUP]):
                # windows下没有SIGKILL, 使用SIGTERM
                os.kill(process.pid, signal.SIGTERM)
                # 无需清理数据，在后置任务中处理
                log.info(f"exchange dag backup abort job success")
                break
        output_action_result(self.pid, ExchangeCode.SUCCESS.value, 0, "")

    def check_circular_logging(self):
        log.info("check circular logging enabled!")
        username = self.m_username.replace("\\", "\\\\")
        db_name = self._json_param.get("job").get("protectObject").get("name")
        cmds = f"{ParamConstant.PS_LOC} {ParamConstant.PS1_SCRIPTS_PATH}GetCircularLoggingEnabled.ps1 \
            {username} {self.m_password} \\\"{db_name}\\\""
        log.info(cmds)
        ret, std_out, std_err = execute_cmd(cmds)
        result = '\n'.join(std_out.splitlines()[-1:])
        if result == "True":
            return True
        return False


def do_work(args_list):
    log.info("do_work start!")
    cmd = args_list[0]
    pid = args_list[1]
    job_id = ""
    sub_job_id = ""
    if len(args_list) >= 3:
        job_id = args_list[2]
    if len(args_list) == 4:
        sub_job_id = args_list[3]
    backup_comm_inst = ExchangeBackupComm(pid, job_id, sub_job_id)

    flags, backup_comm_inst.m_username = get_key_value(f'job_protectEnv_auth_authKey_{pid}')
    if not flags:
        log.error("get username FAILED")
        return False
    flags, backup_comm_inst.m_password = get_key_value(f'job_protectEnv_auth_authPwd_{pid}')
    if not flags:
        log.error("get user password FAILED")
        return False

    cmd_dict = {
        "AllowBackupInLocalNode": [backup_comm_inst.allow_backup_in_local_node],
        "BackupPrerequisite": [backup_comm_inst.backup_prerequisite],
        "Backup": [backup_comm_inst.backup],
        "BackupPostJob": [backup_comm_inst.backup_post_job],
        "QueryJobPermission": [backup_comm_inst.query_job_permission],
        "progress_comm": [backup_comm_inst.progress_comm],
        "BackupGenSubJob": [backup_comm_inst.backup_gen_sub_job],
        "QueryBackupCopy": [backup_comm_inst.query_backup_copy],
        "CheckBackupJobType": [backup_comm_inst.check_backup_job_type],
        'AbortJob': [backup_comm_inst.abort_job]
    }
    func_array = cmd_dict.get(cmd)
    if not func_array:
        output_action_result(pid, ExchangeCode.FAILED.value, BodyErr.ERROR_INTERNAL.value,
                             f"Cmd Param error.")
        return False
    try:
        ret = func_array[0]()
    except Exception as err:
        return False
    finally:
        clear(SysData.SYS_STDIN)
    return ret


if __name__ == "__main__":
    log.info("Running main....")
    for line in sys.stdin:
        SysData.SYS_STDIN = line
        break

    args = sys.argv[1:]
    if len(args) < 2:
        log.error("Not enough parameters, param cnt: %s.", len(args))
        sys.exit(1)
    if not do_work(args):
        sys.exit(0)
